﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.IO
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;
    using System.Linq;

    using Phuse.Text;

    /// <summary>Transparently encodes or decodes data according to
    /// <a href="http://www.ietf.org/rfc/rfc3676.txt">RFC 3676</a>.</summary>
    /// <remarks>
    /// <para>Data passed to any of the Write methods of this stream is assumed to contain paragraph text,
    /// as defined in the first paragraph of <a href="http://www.ietf.org/rfc/rfc3676.txt">RFC 3676</a>, section 3.1.
    /// The passed data is then flowed according to section 4.2. and written to the underlying stream. A call to any of
    /// the Read methods of this stream takes data read from the underlying stream. The data is then interpreted
    /// according to section 4.1., converted to paragraph text and finally returned. For both cases a separate buffer of
    /// the size specified at construction is used, so calls to the Read/Write methods of this class do not directly
    /// translate to calls to the equivalent methods of the underlying stream.</para>
    /// <para><b>Caution</b>: When data is written to this stream, then <see cref="Stream.Dispose()"/> <b>must</b> be
    /// called in the end, see <see cref="Dispose"/> for more information.</para>
    /// </remarks>
    /// <threadsafety static="true" instance="false"/>
    public sealed class FlowedStream : BufferStream
    {
        private static readonly byte[] LineBreakQuote = EncodingObjects.ASCII.GetBytes(" \r\n>");
        private const int LineBreakPastEnd = 3;
        private static readonly byte[] Space = EncodingObjects.ASCII.GetBytes(" ");
        private static readonly byte[] SignatureSeparator = EncodingObjects.ASCII.GetBytes("-- ");
        private static readonly byte[] SignatureSeparatorLine = EncodingObjects.ASCII.GetBytes("-- \r\n");
        private static readonly byte[] FromSpace = EncodingObjects.ASCII.GetBytes("From ");
        private readonly int addedSpaceBeforeLineBreakLength;
        private readonly bool quotedOnly;
        private readonly int encodedCharactersPerLine;

        private DecodingState decodingState = DecodingState.LineFeed;
        private bool decodingIsSoftLineBreak;
        private int decodingPreviousQuoteDepth;
        private int decodingCurrentQuoteDepth;
        private int decodingPostponedCount;
        private int decodingSeparatorPostponedCount;

        private EncodingState encodingState = EncodingState.LineFeed;
        private int encodingLineBreakQuotePastEnd = LineBreakPastEnd;
        private int encodingLineBreakQuoteIndex = LineBreakPastEnd; // Do not write \r\n for the very first line
        private Queue<byte> encodingWordToRead;
        private Queue<byte> encodingWordToWrite = new Queue<byte>();
        private Queue<byte> encodingNextWordToWrite = new Queue<byte>();
        private int encodingColumn;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Initializes a new instance of the <see cref="FlowedStream"/> class by calling
        /// <see cref="FlowedStream(Stream, bool, bool)"/>(<paramref name="stream"/>, true, false).</summary>
        public FlowedStream(Stream stream) : this(stream, Settings.InMemoryStreamBufferSize, true, false)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="FlowedStream"/> class by calling
        /// <see cref="FlowedStream(Stream, int, bool, bool)"/>(<paramref name="stream"/>, 1024,
        /// <paramref name="deleteSpaceBeforeLineBreaks"/>, <paramref name="quotedOnly"/>).</summary>
        public FlowedStream(Stream stream, bool deleteSpaceBeforeLineBreaks, bool quotedOnly) :
            this(stream, Settings.InMemoryStreamBufferSize, deleteSpaceBeforeLineBreaks, quotedOnly)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="FlowedStream"/> class by calling
        /// <see cref="FlowedStream(Stream, int, bool, bool, int)"/>(<paramref name="stream"/>, 1024,
        /// <paramref name="deleteSpaceBeforeLineBreaks"/>, <paramref name="quotedOnly"/>,
        /// <paramref name="encodedCharactersPerLine"/>).</summary>
        public FlowedStream(
            Stream stream,
            bool deleteSpaceBeforeLineBreaks,
            bool quotedOnly,
            int encodedCharactersPerLine) :
            this(stream, Settings.InMemoryStreamBufferSize, deleteSpaceBeforeLineBreaks, quotedOnly, encodedCharactersPerLine)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="FlowedStream"/> class by calling
        /// <see cref="FlowedStream(Stream, int, bool, bool, int)"/>(<paramref name="stream"/>,
        /// <paramref name="bufferSize"/>, <paramref name="deleteSpaceBeforeLineBreaks"/>, <paramref name="quotedOnly"/>,
        /// 66).</summary>
        public FlowedStream(Stream stream, int bufferSize, bool deleteSpaceBeforeLineBreaks, bool quotedOnly) :
            this(stream, bufferSize, deleteSpaceBeforeLineBreaks, quotedOnly, 66)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="FlowedStream"/> class.</summary>
        /// <param name="stream">The stream from/to which encoded data is read/written.</param>
        /// <param name="bufferSize">The size of the internal read and write buffers in bytes.</param>
        /// <param name="deleteSpaceBeforeLineBreaks">The value <c>true</c> is interpreted as <c>DelSp=Yes</c>;
        /// <c>false</c> means <c>DelSp=No</c>, as defined in
        /// <a href="http://www.ietf.org/rfc/rfc3676.txt">RFC 3676</a>, section 4.</param>
        /// <param name="quotedOnly">Encode/decode only the quoted bits of the data (Lines starting with the &gt;
        /// character). Unquoted parts are passed through as is.</param>
        /// <param name="encodedCharactersPerLine">The number of characters per line that <strong>should</strong> be
        /// written to the underlying stream. Note that a low value for <paramref name="encodedCharactersPerLine"/>
        /// and/or high quoting depth and/or long words may lead to lines that are longer than this limit, up to the
        /// maximum of 998 characters, as specified in <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a>
        /// 2.1.1.).</param>
        /// <exception cref="ArgumentNullException"><paramref name="stream"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="bufferSize"/> and/or
        /// <paramref name="encodedCharactersPerLine"/> is less than or equal to 0.</exception>
        public FlowedStream(
            Stream stream,
            int bufferSize,
            bool deleteSpaceBeforeLineBreaks,
            bool quotedOnly,
            int encodedCharactersPerLine) :
            base(stream, bufferSize)
        {
            if (encodedCharactersPerLine <= 0)
            {
                throw new ArgumentOutOfRangeException("encodedCharactersPerLine", ExceptionMessages.PositiveNumberRequired);
            }

            this.addedSpaceBeforeLineBreakLength = deleteSpaceBeforeLineBreaks ? 1 : 0;
            this.quotedOnly = quotedOnly;

            this.encodedCharactersPerLine = encodedCharactersPerLine - this.addedSpaceBeforeLineBreakLength;
            this.encodingWordToRead = this.encodingWordToWrite;
        }

        /// <summary>See <see cref="Stream.Read(byte[], int, int)"/>.</summary>
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Putting the whole state machine into one method improves performance considerably.")]
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated with call to BufferHelper.AssertValidRange.")]
        public sealed override int Read(byte[] buffer, int offset, int count)
        {
            this.AssertNotDisposed();
            BufferHelper.AssertValidRange(buffer, "buffer", offset, "offset", count, "count");

            unchecked
            {
                ReadBuffer readBuffer = this.ReadBuffer;
                int index = offset;
                int pastEnd = offset + count;

                // Only try to get more encoded data if we haven't yet copied anything into buffer
                while ((index < pastEnd) && ((readBuffer.Index < readBuffer.Count) ||
                    ((index == offset) && this.IsMoreEncodedDataAvailable(readBuffer))))
                {
                    switch (this.decodingState)
                    {
                        case DecodingState.CarriageReturn:
                            this.decodingPreviousQuoteDepth = this.decodingCurrentQuoteDepth;
                            this.decodingCurrentQuoteDepth = 0;

                            // No matter what the current byte is, we'll act as if it is an LF and adjust the index
                            // accordingly
                            this.decodingState = DecodingState.LineFeed;
                            ++this.decodingPostponedCount;

                            if (readBuffer[readBuffer.Index] == (byte)'\n')
                            {
                                ++readBuffer.Index;
                            }

                            break;
                        case DecodingState.LineFeed:
                            switch (readBuffer[readBuffer.Index])
                            {
                                case (byte)'>':
                                    this.decodingState = DecodingState.QuoteMark;
                                    break;
                                case (byte)' ':
                                    this.decodingState = DecodingState.LastControl;

                                    if (!this.quotedOnly)
                                    {
                                        ++readBuffer.Index; // Undo space-stuffing
                                    }

                                    break;
                                default:
                                    this.decodingState = DecodingState.LastControl;
                                    break;
                            }

                            break;
                        case DecodingState.QuoteMark:
                            switch (readBuffer[readBuffer.Index])
                            {
                                case (byte)'>':
                                    ++this.decodingPostponedCount;
                                    ++this.decodingCurrentQuoteDepth;
                                    ++readBuffer.Index;
                                    break;
                                case (byte)' ':
                                    this.decodingState = DecodingState.LastControl;
                                    ++readBuffer.Index; // Undo space-stuffing
                                    break;
                                default:
                                    this.decodingState = DecodingState.LastControl;
                                    break;
                            }

                            break;
                        case DecodingState.LastControl:
                            if (this.decodingSeparatorPostponedCount == SignatureSeparatorLine.Length)
                            {
                                this.decodingState = DecodingState.AddPostponed;
                            }
                            else
                            {
                                if (readBuffer[readBuffer.Index] ==
                                    SignatureSeparatorLine[this.decodingSeparatorPostponedCount])
                                {
                                    ++readBuffer.Index;
                                    ++this.decodingPostponedCount;
                                    ++this.decodingSeparatorPostponedCount;
                                }
                                else
                                {
                                    this.decodingState = DecodingState.AddPostponed;
                                }
                            }

                            break;
                        case DecodingState.Normal:
                            var currentByte = readBuffer[readBuffer.Index++];

                            switch (currentByte)
                            {
                                case (byte)'\r':
                                    this.decodingState = DecodingState.CarriageReturn;
                                    this.decodingIsSoftLineBreak = false;
                                    ++this.decodingPostponedCount;
                                    break;
                                case (byte)' ':
                                    if (this.quotedOnly && (this.decodingCurrentQuoteDepth == 0))
                                    {
                                        buffer[index++] = currentByte;
                                    }
                                    else
                                    {
                                        this.decodingState = DecodingState.Space;
                                    }

                                    break;
                                default:
                                    buffer[index++] = currentByte;
                                    break;
                            }

                            break;
                        case DecodingState.Space:
                            switch (readBuffer[readBuffer.Index])
                            {
                                case (byte)'\r':
                                    this.decodingState = DecodingState.CarriageReturn;
                                    this.decodingIsSoftLineBreak = true;
                                    ++this.decodingPostponedCount;
                                    ++readBuffer.Index;
                                    break;
                                case (byte)' ':
                                    buffer[index++] = (byte)' ';
                                    ++readBuffer.Index;
                                    break;
                                default:
                                    this.decodingState = DecodingState.Normal;
                                    buffer[index++] = (byte)' ';
                                    break;
                            }

                            break;
                        default: // This should be DecodingState.AddPostponed but doing so decreases code coverage
                            if (this.decodingPostponedCount == 0)
                            {
                                if (this.decodingSeparatorPostponedCount > 0)
                                {
                                    // If we encountered a (possibly incomplete) separator, then make sure that the
                                    // state is set accordingly.
                                    switch (SignatureSeparatorLine[this.decodingSeparatorPostponedCount - 1])
                                    {
                                        case (byte)'\r':
                                            this.decodingState = DecodingState.CarriageReturn;
                                            break;
                                        case (byte)'\n':
                                            this.decodingState = DecodingState.LineFeed;
                                            break;
                                        default:
                                            this.decodingState = DecodingState.Normal;
                                            break;
                                    }
                                }
                                else
                                {
                                    this.decodingState = DecodingState.Normal;
                                }

                                this.decodingSeparatorPostponedCount = 0;
                            }
                            else if (this.decodingIsSoftLineBreak)
                            {
                                this.decodingIsSoftLineBreak = false;

                                // See http://www.ietf.org/rfc/rfc3676.txt, 4.1./7
                                if ((this.decodingCurrentQuoteDepth == this.decodingPreviousQuoteDepth) &&
                                    (this.decodingSeparatorPostponedCount != SignatureSeparatorLine.Length))
                                {
                                    if (this.addedSpaceBeforeLineBreakLength == 0)
                                    {
                                        buffer[index++] = (byte)' ';
                                    }

                                    this.decodingPostponedCount = this.decodingSeparatorPostponedCount;
                                }
                            }
                            else
                            {
                                // postponedIndex > 0 indicates that the next byte is part of the line break or quoting
                                // postponedIndex <= 0 indicates that the next byte is part of a signature separator
                                var postponedIndex = this.decodingPostponedCount - this.decodingSeparatorPostponedCount;
                                byte postponed;

                                if (postponedIndex <= 0)
                                {
                                    postponed = SignatureSeparatorLine[-postponedIndex];
                                }
                                else
                                {
                                    switch (postponedIndex - this.decodingCurrentQuoteDepth)
                                    {
                                        case 2:
                                            postponed = (byte)'\r';
                                            break;
                                        case 1:
                                            postponed = (byte)'\n';
                                            break;
                                        default:
                                            postponed = (byte)'>';
                                            break;
                                    }
                                }

                                buffer[index++] = postponed;
                                --this.decodingPostponedCount;
                            }

                            break;
                    }
                }

                return index - offset;
            }
        }

        /// <summary>See <see cref="Stream.Write(byte[], int, int)"/>.</summary>
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Putting the whole state machine into one method improves performance considerably.")]
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated with call to BufferHelper.AssertValidRange.")]
        public sealed override void Write(byte[] buffer, int offset, int count)
        {
            this.AssertNotDisposed();
            BufferHelper.AssertValidRange(buffer, "buffer", offset, "offset", count, "count");

            unchecked
            {
                WriteBuffer writeBuffer = this.WriteBuffer;
                int pastEnd = offset + count;
                byte currentByte;

                while ((offset < pastEnd) && ((writeBuffer.Count < writeBuffer.Capacity) || writeBuffer.Flush()))
                {
                    switch (this.encodingState)
                    {
                        case EncodingState.CarriageReturn:
                            // No matter what the current byte is, we'll act as if it is an LF and adjust the index
                            // accordingly
                            this.encodingState = EncodingState.LineFeed;

                            if (buffer[offset] == (byte)'\n')
                            {
                                ++offset;
                            }

                            break;
                        case EncodingState.LineFeed:
                            this.encodingState =
                                buffer[offset] == (byte)'>' ? EncodingState.QuoteMark : EncodingState.AddLineBreak;
                            break;
                        case EncodingState.QuoteMark:
                            if (buffer[offset] == (byte)'>')
                            {
                                ++this.encodingLineBreakQuotePastEnd;
                                ++offset;
                            }
                            else
                            {
                                this.encodingState = EncodingState.AddLineBreak;
                            }

                            break;
                        case EncodingState.AddLineBreak:
                            if (this.encodingLineBreakQuoteIndex == this.encodingLineBreakQuotePastEnd)
                            {
                                this.encodingState = EncodingState.Normal;
                                this.encodingLineBreakQuoteIndex = 1 - this.addedSpaceBeforeLineBreakLength;

                                if (this.encodingLineBreakQuotePastEnd > LineBreakPastEnd)
                                {
                                    // Always start quoted lines with a stuff space
                                    ++this.encodingColumn;
                                    writeBuffer[writeBuffer.Count++] = (byte)' ';
                                }
                            }
                            else
                            {
                                byte toWrite = LineBreakQuote[Math.Min(
                                    LineBreakQuote.Length - 1, this.encodingLineBreakQuoteIndex++)];
                                this.encodingColumn = toWrite == (byte)'\n' ? 0 : this.encodingColumn + 1;
                                writeBuffer[writeBuffer.Count++] = toWrite;
                            }

                            break;
                        case EncodingState.Normal:
                            currentByte = buffer[offset];

                            switch (currentByte)
                            {
                                case (byte)' ':
                                    this.encodingState = EncodingState.Space;
                                    break;
                                case (byte)'\r':
                                    if (this.encodingWordToWrite.Count == 0)
                                    {
                                        this.encodingState = EncodingState.CarriageReturn;
                                        this.encodingLineBreakQuoteIndex = 1; // Do not write a space for a real CRLF
                                        this.encodingLineBreakQuotePastEnd = LineBreakPastEnd;
                                        this.encodingWordToRead = this.encodingWordToWrite;

                                        ++offset;
                                    }
                                    else
                                    {
                                        this.encodingState = EncodingState.AddWord;
                                    }

                                    break;
                                default:
                                    this.encodingWordToRead.Enqueue(currentByte);
                                    ++offset;
                                    break;
                            }

                            break;
                        case EncodingState.Space:
                            currentByte = buffer[offset];

                            switch (currentByte)
                            {
                                case (byte)' ':
                                    this.encodingWordToRead.Enqueue(currentByte);
                                    ++offset;
                                    break;
                                case (byte)'\r':
                                    this.encodingState = EncodingState.AddWord;
                                    break;
                                default:
                                    if (this.encodingWordToRead == this.encodingWordToWrite)
                                    {
                                        this.encodingWordToRead = this.encodingNextWordToWrite;
                                        this.encodingState = EncodingState.Normal;
                                    }
                                    else
                                    {
                                        this.encodingState = EncodingState.AddWord;
                                    }

                                    break;
                            }

                            break;
                        default: // This should be EncodingState.AddWord but doing so decreases code coverage
                            if (this.encodingWordToWrite.Count > 0)
                            {
                                if ((this.encodingColumn == 0) && this.IsStuffingRequired)
                                {
                                    writeBuffer[writeBuffer.Count++] = (byte)' ';
                                    ++this.encodingColumn;
                                }
                                else
                                {
                                    writeBuffer[writeBuffer.Count++] = this.encodingWordToWrite.Dequeue();
                                    ++this.encodingColumn;
                                }
                            }
                            else
                            {
                                if (this.IsLineBreakRequired)
                                {
                                    this.encodingState = EncodingState.AddLineBreak;
                                }
                                else
                                {
                                    this.encodingState = EncodingState.Normal;
                                }

                                this.MoveToNextWord();
                            }

                            break;
                    }
                }
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="Stream.Dispose(bool)"/>.</summary>
        /// <remarks>If <paramref name="disposing"/> is <c>true</c> then the last words are written into the underlying
        /// stream.</remarks>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Dispose() must never throw.")]
        protected sealed override void Dispose(bool disposing)
        {
            try
            {
                if (disposing && !this.IsDisposed)
                {
                    // Unfortunately, some of what follows is duplicated from the Write method. This could only be
                    // avoided at the cost of sacrificing quite a bit of execution speed in the emulator (and thus
                    // on devices).
                    switch (this.encodingState)
                    {
                        case EncodingState.CarriageReturn:
                        case EncodingState.LineFeed:
                        case EncodingState.QuoteMark:
                            this.WriteLineBreak();
                            break;
                        default:
                            this.WriteCurrentWord();

                            if (this.IsLineBreakRequired)
                            {
                                this.WriteLineBreak();
                            }

                            this.MoveToNextWord();
                            this.WriteCurrentWord();
                            break;
                    }
                }
            }
            catch
            {
            }
            finally
            {
                base.Dispose(disposing);
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private bool IsMoreEncodedDataAvailable(ReadBuffer readBuffer)
        {
            // The test against this.decodingState should more logically appear at the second position in the while
            // loop in the Read method because there's no need test whether there's more encoded data available in
            // the buffer when the next byte will be drawn from the postpone queue anyway. However, doing so would
            // mean that 3 tests must be made for *each* byte in the encoded stream. Doing this test here means that
            // this property getter is called only when the buffer is empty and until the postpone queue has been
            // emptied. This leads to slightly more calls to this property getter, but in turn we eliminate one test
            // from the while loop.
            if ((this.decodingState == DecodingState.AddPostponed) || readBuffer.Fill())
            {
                return true;
            }

            this.decodingState = DecodingState.AddPostponed;
            return this.decodingPostponedCount > 0;
        }

        private bool IsStuffingRequired
        {
            get
            {
                return !this.quotedOnly && ((this.encodingWordToWrite.Peek() == (byte)' ') ||
                    ((this.encodingWordToWrite.Count >= FromSpace.Length) &&
                    this.encodingWordToWrite.Take(FromSpace.Length).SequenceEqual(FromSpace)));
            }
        }

        private bool IsLineBreakRequired
        {
            get
            {
                // TODO: Ensure no line is longer than 998 characters
                return (!this.quotedOnly || (this.encodingLineBreakQuotePastEnd != LineBreakPastEnd)) &&
                    (this.encodingNextWordToWrite.Count > 0) &&
                    (this.encodingColumn + this.encodingNextWordToWrite.Count > this.encodedCharactersPerLine) &&
                    ((this.encodingNextWordToWrite.Count != SignatureSeparator.Length) ||
                    !this.encodingNextWordToWrite.SequenceEqual(SignatureSeparator));
            }
        }

        private void MoveToNextWord()
        {
            this.encodingWordToRead = this.encodingWordToWrite;
            this.encodingWordToWrite = this.encodingNextWordToWrite;
            this.encodingNextWordToWrite = this.encodingWordToRead;
        }

        private void WriteLineBreak()
        {
            var lineBreakLength = LineBreakPastEnd - this.encodingLineBreakQuoteIndex;
            var quoteDepth = this.encodingLineBreakQuotePastEnd - LineBreakPastEnd;
            var ourLineBreak = LineBreakQuote.Skip(this.encodingLineBreakQuoteIndex).Take(lineBreakLength).Concat(
                Enumerable.Repeat((byte)'>', quoteDepth));
            this.Write(ourLineBreak);
        }

        private void WriteCurrentWord()
        {
            if ((this.encodingColumn == 0) && this.IsStuffingRequired)
            {
                this.Write(Space);
            }

            this.Write(this.encodingWordToWrite);
        }

        private void Write(IEnumerable<byte> sequence)
        {
            using (var enumerator = sequence.GetEnumerator())
            {
                // Note: Below, unlike in the Write method, we don't use a local variable for the WriteBuffer object
                // because doing so might unnecessarily allocate such an object, even when nothing was ever written
                // to this stream.
                while (enumerator.MoveNext() &&
                    ((this.WriteBuffer.Count < this.WriteBuffer.Capacity) || this.WriteBuffer.Flush()))
                {
                    byte toWrite = enumerator.Current;
                    this.WriteBuffer[this.WriteBuffer.Count++] = toWrite;
                    this.encodingColumn = toWrite == (byte)'\n' ? 0 : this.encodingColumn + 1;
                }
            }
        }

        private enum DecodingState
        {
            /// <summary>The previous read byte was a CR.</summary>
            CarriageReturn,

            /// <summary>The previous read byte was a LF.</summary>
            LineFeed,

            /// <summary>The previous read byte was a quote mark ('>'), which was postponed.</summary>
            QuoteMark,

            /// <summary>The previous read byte was the last control character ('>' or stuffing space at the beginning
            /// of the line) or a character that might belong to a signature separator.</summary>
            LastControl,

            /// <summary>The previous read byte was a normal byte.</summary>
            Normal,

            /// <summary>The previous read byte was a normal (non-stuff) space (' '), which was neither postponed nor
            /// put into the buffer.</summary>
            Space,

            /// <summary>A soft line break was found to be a hard one because it was followed by a line with a different
            /// quoting depth or a signature line -> add postponed bytes.</summary>
            AddPostponed
        }

        private enum EncodingState
        {
            /// <summary>The previous byte was a CR.</summary>
            CarriageReturn,

            /// <summary>The previous byte was a LF.</summary>
            LineFeed,

            /// <summary>The previous byte was a quote mark ('>').</summary>
            QuoteMark,

            /// <summary>The previous byte written to the underlying stream was part of a line break (CR, LF, quote mark
            /// or stuff space).</summary>
            AddLineBreak,

            /// <summary>The previous byte was part of a word and was put into the current word queue.</summary>
            Normal,

            /// <summary>The previous byte was word-separating white space and was put into the current word queue.</summary>
            Space,

            /// <summary>The previous byte written to the underlying stream was taken from the current word queue.
            /// </summary>
            AddWord
        }
    }
}
